﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using RconLibrary.Protocol;
using System.Diagnostics;

namespace RconLibrary
{
    /// <summary>
    /// Rcon connection class.
    /// </summary>
    public class Connection
    {
        #region Properties

        /// <summary>
        /// Gets the RCON address.
        /// </summary>
        public string Address
        {
            get
            {
                return this.host;
            }
        }

        /// <summary>
        /// Gets the RCON address.
        /// </summary>
        public int Port
        {
            get
            {
                return this.port;
            }
        }

        /// <summary>
        /// Gets the connection state.
        /// </summary>
        public ConnectionState State
        {
            get
            {
                return this.state;
            }
            private set
            {
                bool changed = this.state != value;
                this.state = value;
                if (changed)
                {
                    this.OnStateChanged();
                }
            }
        }

        #endregion

        #region Events

        /// <summary>
        /// Void event handler.
        /// </summary>
        public delegate void VoidHandler();

        /// <summary>
        /// Connected event.
        /// </summary>
        public event VoidHandler Connected;

        /// <summary>
        /// Connected event dispatcher.
        /// </summary>
        protected virtual void OnConnected()
        {
            this.State = ConnectionState.Connected;
            if (this.Connected != null)
            {
                this.Connected();
            }
        }

        /// <summary>
        /// Connecting event.
        /// </summary>
        public event VoidHandler Connecting;

        /// <summary>
        /// Connecting event dispatcher.
        /// </summary>
        protected virtual void OnConnecting()
        {
            this.State = ConnectionState.Connecting;
            if (this.Connecting != null)
            {
                this.Connecting();
            }
        }

        /// <summary>
        /// Reconnecting event.
        /// </summary>
        public event VoidHandler Reconnecting;

        /// <summary>
        /// Connecting event dispatcher.
        /// </summary>
        protected virtual void OnReconnecting()
        {
            this.State = ConnectionState.Reconnecting;
            if (this.Reconnecting != null)
            {
                this.Reconnecting();
            }
        }

        /// <summary>
        /// Connection error args.
        /// </summary>
        public class ConnectionEventArgs
        {
            /// <summary>
            /// Reconnects the client or not?
            /// </summary>
            public bool Reconnect 
            {
                get;
                set;
            }
        }

        /// <summary>
        /// Connection error handler.
        /// </summary>
        public delegate void ConnectionHandler(ConnectionEventArgs args);

        /// <summary>
        /// Connection error event.
        /// </summary>
        public event ConnectionHandler ConnectFailed;

        /// <summary>
        /// Connection error event dispatcher.
        /// </summary>
        protected virtual bool OnConnectError()
        {
            this.State = ConnectionState.Error;
            if (this.ConnectFailed != null)
            {
                var args = new ConnectionEventArgs();
                this.ConnectFailed(args);
                if (args.Reconnect)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Disconnected event.
        /// </summary>
        public event ConnectionHandler Disconnected;

        /// <summary>
        /// Disconnected event dispatcher.
        /// </summary>
        protected virtual bool OnDisconnected()
        {
            lock (this)
            {
                if (this.state == ConnectionState.Closed)
                {
                    return false;
                }
                this.State = ConnectionState.Closed;
                if (this.Disconnected != null)
                {
                    var args = new ConnectionEventArgs();
                    this.Disconnected(args);
                    if (args.Reconnect)
                    {
                        return true;
                    }
                }
                return false;
            }
        }

        /// <summary>
        /// Connection error handler.
        /// </summary>
        public delegate void ErrorHandler(Exception e);

        /// <summary>
        /// Error event.
        /// </summary>
        public event ErrorHandler Error;

        /// <summary>
        /// Error event dispatcher
        /// </summary>
        /// <returns></returns>
        protected virtual void OnError(Exception e)
        {
            this.State = ConnectionState.Error;
            if (this.Error != null)
            {
                this.Error(e);
            }
        }

        /// <summary>
        /// State changed event.
        /// </summary>
        public event VoidHandler StateChanged;

        /// <summary>
        /// State changed event dispatcher.
        /// </summary>
        protected virtual void OnStateChanged()
        {
            if (this.StateChanged != null)
            {
                this.StateChanged();
            }
        }

        /// <summary>
        /// Request event.
        /// </summary>
        public event Processor.PacketHandler RequestPacket;

        /// <summary>
        /// Request event dispatcher.
        /// </summary>
        /// <param name="packet"></param>
        protected virtual void OnRequestPacket(Packet packet)
        {
            if (this.RequestPacket != null)
            {
                this.RequestPacket(packet);
            }
        }

        /// <summary>
        /// Request event.
        /// </summary>
        public event Processor.PacketHandler ResponsePacket;

        /// <summary>
        /// Response event dispatcher.
        /// </summary>
        /// <param name="packet"></param>
        protected virtual void OnResponsePacket(Packet packet)
        {
            if (this.ResponsePacket != null)
            {
                this.ResponsePacket(packet);
            }
        }

        #endregion

        #region Variables

        /// <summary>
        /// Host address.
        /// </summary>
        private string host = "";

        /// <summary>
        /// Host port.
        /// </summary>
        private int port = 0;

        /// <summary>
        /// Network state.
        /// </summary>
        private ConnectionState state = ConnectionState.Closed;

        /// <summary>
        /// Network socket.
        /// </summary>
        private TcpClient socket = null;

        /// <summary>
        /// Protocol handler.
        /// </summary>
        private Processor protocol = new Processor();

        /// <summary>
        /// Received packets.
        /// </summary>
        private Queue<Packet> packets = new Queue<Packet>();

        /// <summary>
        /// Incoming buffer.
        /// </summary>
        private byte[] receiveBuffer = new byte[4096];

        /// <summary>
        /// Output buffer.
        /// </summary>
        private byte[] sendBuffer = new byte[4096];

        /// <summary>
        /// Output buffer synchronization.
        /// </summary>
        private Object sendBufferSync = new Object();

        /// <summary>
        /// Size of data pending in the output buffer.
        /// </summary>
        private int sendBufferSize = 0;

        /// <summary>
        /// Current connection sequence.
        /// </summary>
        private int sequence = 0;

        /// <summary>
        /// Sequence generation synchronization.
        /// </summary>
        private Object sequenceSync = new Object();

        #endregion

        /// <summary>
        /// Creates a new rcon connection instance.
        /// </summary>
        /// <param name="host">Server address.</param>
        /// <param name="port">Server port.</param>
        public Connection(string host, int port)
        {
            this.host = host;
            this.port = port;
            this.protocol.Request += this.OnRequestPacket;
            this.protocol.Response += this.OnResponsePacket;
        }

        /// <summary>
        /// Connects to the server.
        /// </summary>
        public void Connect()
        {
            if (this.state != ConnectionState.Closed && this.state != ConnectionState.Error)
            {
                return;
            }
            
            this.OnConnecting();

            this.socket = new TcpClient();
            this.socket.BeginConnect(this.host, this.port, new AsyncCallback(this.ConnectCallback), this.socket);
        }

        /// <summary>
        /// Connection callback.
        /// </summary>
        /// <param name="ar">Callback information.</param>
        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                TcpClient client = (TcpClient)ar.AsyncState;
                client.EndConnect(ar);

                lock (this.sequenceSync)
                {
                    this.sequence = 0;
                }
                
                this.OnConnected();
                this.Receive();
            }
            catch (System.Exception)
            {
                if (this.OnConnectError())
                {
                    this.Connect();
                }
            }
        }

        /// <summary>
        /// Receives data from the server.
        /// </summary>
        private void Receive()
        {
            this.socket.Client.BeginReceive(this.receiveBuffer, 0, this.receiveBuffer.Length, SocketFlags.None, new AsyncCallback(this.ReceiveCallback), this.socket);
        }

        /// <summary>
        /// Receive callback.
        /// </summary>
        /// <param name="ar">Callback information.</param>
        private void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                TcpClient client = (TcpClient)ar.AsyncState;
                int length = client.Client.EndReceive(ar);
                if (length > 0)
                {
                    this.protocol.Process(this.receiveBuffer.Slice(0, length));
                    this.Receive();
                }
            }
            catch (SocketException)
            {
                this.Disconnect();
                if (this.OnDisconnected())
                {
                    this.Connect();
                }
            }
            catch (Exception e)
            {
                this.Disconnect();
                this.OnError(e);
            }
        }


        /// <summary>
        /// Sends the raw bytes to the server.
        /// </summary>
        protected void Send(byte[] data)
        {
            if (this.state != ConnectionState.Connected)
            {
                return;
            }

            if (this.socket == null)
            {
                return;
            }

            lock (this.sendBufferSync)
            {
                Buffer.BlockCopy(data, 0, this.sendBuffer, this.sendBufferSize, data.Length);
                this.sendBufferSize += data.Length;
                this.socket.Client.BeginSend(this.sendBuffer, 0, this.sendBufferSize, SocketFlags.None, new AsyncCallback(this.SendCallback), this.socket);
            }
        }

        /// <summary>
        /// Send
        /// </summary>
        /// <param name="ar"></param>
        private void SendCallback(IAsyncResult ar)
        {
            try
            {
                TcpClient client = (TcpClient)ar.AsyncState;
                int length = client.Client.EndSend(ar);
                if (length > 0)
                {
                    lock (this.sendBufferSync)
                    {
                        if (length < this.sendBufferSize)
                        {
                            byte[] remaining = new byte[this.sendBufferSize - length];
                            Buffer.BlockCopy(this.sendBuffer, length, remaining, 0, remaining.Length);
                            Buffer.BlockCopy(remaining, 0, this.sendBuffer, 0, remaining.Length);
                            this.sendBufferSize -= length;
                            this.socket.Client.BeginSend(this.sendBuffer, 0, this.sendBufferSize, SocketFlags.None, new AsyncCallback(this.SendCallback), this.socket);
                        }
                        else
                        {
                            this.sendBufferSize = 0;
                        }
                    }
                }
                else
                {
                    Debugger.Break();
                }
            }
            catch (SocketException)
            {
                this.Disconnect();
                if (this.OnDisconnected())
                {
                    this.Connect();
                }
            }
            catch (Exception e)
            {
                this.Disconnect();
                this.OnError(e);
            }
        }

        /// <summary>
        /// Disconnects the socket.
        /// </summary>
        public void Disconnect()
        {
            if (this.socket != null)
            {
                try
                {
                    this.socket.GetStream().Close();
                    this.socket.Close();
                }
                catch (System.Exception)
                {
                }
                finally
                {
                    this.socket = null;
                }
            }
        }

        /// <summary>
        /// Create and send a request packet to the server.
        /// </summary>
        /// <param name="parameters">List of objects.</param>
        public void Request(params object[] parameters)
        {
            lock (this.sequenceSync)
            {
                Packet packet = new Packet(this.sequence++, PacketOrigin.Client, PacketType.Request, parameters);
                this.Send(packet.Encode());
            }
        }

        /// <summary>
        /// Create and send a response packet to the server.
        /// </summary>
        /// <param name="request">Request packet from the server.</param>
        /// <param name="parameters">List of parameters to put in the packet.</param>
        public void Respond(Packet request, params object[] parameters)
        {
            Packet packet = new Packet(request.Sequence, request.Origin, PacketType.Response, parameters);
            this.Send(packet.Encode());
        }

    }

}
